/*

 Copyright 2013 Chris Wilson
 Copyright 2014-2015 Takashi Mizuhiki

 Licensed under the Apache License, Version 2.0 (the "License");
 you may not use this file except in compliance with the License.
 You may obtain a copy of the License at

 http://www.apache.org/licenses/LICENSE-2.0

 Unless required by applicable law or agreed to in writing, software
 distributed under the License is distributed on an "AS IS" BASIS,
 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 See the License for the specific language governing permissions and
 limitations under the License.

*/

// This work was derived from WebMIDIAPIShim by Chris Wilson.
// https://github.com/cwilso/WebMIDIAPIShim

(function (global) {
    function Iterator(items) {
        this._items = items;
        this._index = 0;
        this._maxIndex = items.length;
    }

    Iterator.prototype.next = function(){
        if (this._index === this._maxIndex) {
            return {value: undefined, done: true};
        }

        return {value: this._items[this._index++], done: false};
    };

    Iterator.prototype.reset = function(){
        this._index = 0;
    };


    function Promise() {
    }

    Promise.prototype.then = function(accept, reject) {
        this.accept = accept;
        this.reject = reject;
    };

    Promise.prototype.succeed = function(access) {
        if (this.accept)
            this.accept(access);
    };

    Promise.prototype.fail = function(error) {
        if (this.reject)
            this.reject(error);
    };

    var MIDIPortDeviceState = {
        disconnected : "disconnected",
        connected    : "connected"
    };

    var MIDIPortConnectionState = {
        open    : "open",
        closed  : "closed",
        pending : "pending"
    };

    MIDIEventDispatcher = function MIDIEventDispatcher() {
        this._listeners = {};
    };

    MIDIEventDispatcher.prototype.addEventListener = function (type, listener, useCapture) {
        var listeners = this._listeners[type];
        if (listeners != null) {
            for (var i = 0; i < listeners.length; i++) {
                if (listeners[i] == listener) {
                    return;
                }
            }
        } else {
            this._listeners[type] = [];
        }

        this._listeners[type].push( listener );
    };

    MIDIEventDispatcher.prototype.removeEventListener = function (type, listener, useCapture) {
        var listeners = this._listeners[type];
        if (listeners != null) {
            for (var i = 0; i < listeners.length; i++) {
                if (listeners[i] == listener) {
                    this._listeners[type].splice( i, 1 );  //remove it
                    return;
                }
            }
        }
    };

    MIDIEventDispatcher.prototype.preventDefault = function() {
        this._pvtDef = true;
    };

    MIDIEventDispatcher.prototype.dispatchEvent = function (evt) {
        this._pvtDef = false;

        var listeners = this._listeners[evt.type];
        if (listeners != null) {
            // dispatch to listeners
            for (var i = 0; i < listeners.length; i++) {
                if (listeners[i].handleEvent) {
                    listeners[i].handleEvent.bind(this)( evt );
                } else {
                    listeners[i].bind(this)( evt );
                }
            }
        }

        switch (evt.type) {
        case "midimessage":
            if (this.onmidimessage) {
                this.onmidimessage( evt );
            }
            break;

        case "statechange":
            if (this.onstatechange) {
                this.onstatechange( evt );
            }
            break;
        }

        return this._pvtDef;
    };

    MIDIAccess = function(options) {
        this._promise = new Promise;
        this._sources = null;
        this._destinations = null;
        this._inputs = null;
        this._outputs = null;
        this._timestampOrigin = 0;
        this._sysexAccessRequested = false;
        this.onstatechange = null;
        this.sysexEnabled = false;
        this.inputs = null;
        this.outputs = null;
        var _this = this;

        _callback_onReady = function(m_sources, m_destinations) {
            _this._timestampOrigin = window.performance.now();

            console.log("m_sources[" + m_sources + "]m_destinations[" + m_destinations + "]");
            var sources = JSON.parse(m_sources);

            var inputs = new Array(sources.length);
            for (var i = 0; i < sources.length; i++ ) {
                inputs[i] = new MIDIInput( sources[i].id, sources[i].name, sources[i].manufacturer, i, _this );
            }

            _this._inputs = inputs;
            _this.inputs = _createMIDIPortMap(inputs);

            destinations = JSON.parse(m_destinations);
            var outputs = new Array(destinations.length);
            for (var i = 0; i < destinations.length; i++ ) {
                outputs[i] = new MIDIOutput( destinations[i].id, destinations[i].name, destinations[i].manufacturer, i, _this );
            }

            _this._outputs = outputs;
            _this.outputs = _createMIDIPortMap(outputs);

            _this.sysexEnabled = _this._sysexAccessRequested;

            _onReady.bind(_this)();
        };

        _callback_onNotReady = function() {
            _onNotReady.bind(_this)();
        };

        _callback_receiveMIDIMessage = function(index, receivedTime, rawdata, len) {
            function stringToBytes(str) {
                var ch, st, re = [];
                for (var i = 0; i < str.length; i++ ) {
                    ch = str.charCodeAt(i);  // get char
                    st = [];                 // set up "stack"
                    do {
                        st.push( ch & 0xFF );  // push byte to stack
                        ch = ch >> 8;          // shift value down by 1 byte
                    } while (ch);
                    // add stack contents to result
                    // done because chars have "wrong" endianness
                    re = re.concat(st.reverse());
                }

                // return an array of bytes
                return re;
            }

            var data = stringToBytes(atob(rawdata));
            var evt = document.createEvent( "Event" );


            evt.initEvent( "midimessage", false, false );
            if (receivedTime !== '0.0') {
                evt.receivedAt = parseFloat(receivedTime) + _this._timestampOrigin;
            }
            evt.data = data;
            console.log("_callback_receiveMIDIMessage[" + receivedTime + "][" + _this._timestampOrigin + "][" + evt.receivedAt + "]");

            var input = _this._inputs[index];
            if (input != null) {
                input.dispatchEvent(evt);
            }
        };

        _callback_addDestination = function(index, m_portInfo) {
            var portInfo = JSON.parse(m_portInfo);
            var evt = document.createEvent( "Event" );
            var output = new MIDIOutput(portInfo.id, portInfo.name, portInfo.manufacturer, index, _this);

            _this._outputs.splice(index, 0, output);
            _this.outputs = _createMIDIPortMap(_this._outputs);

            output._setState(MIDIPortDeviceState.connected, false);
            output._setConnection(MIDIPortConnectionState.closed, true);

            evt.initEvent( "statechange", false, false );
            evt.port = output;
            _this.dispatchEvent(evt);
        };

        _callback_addSource = function(index, m_portInfo) {
            var portInfo = JSON.parse(m_portInfo);
            var evt = document.createEvent( "Event" );
            var input = new MIDIInput(portInfo.id, portInfo.name, portInfo.manufacturer, index, _this);

            _this._inputs.splice(index, 0, input);
            _this.inputs = _createMIDIPortMap(_this._inputs);

            input._setState(MIDIPortDeviceState.connected, false);
            input._setConnection(MIDIPortConnectionState.closed, true);

            evt.initEvent( "statechange", false, false );
            evt.port = input;
            _this.dispatchEvent(evt);
        };

        _callback_removeDestination = function(index) {
            if (!_this._outputs || _this._outputs.length == 0 || index > _this._outputs.length - 1) {
                console.log('_callback_removeDestination failed: ' + index);
                return;
            }

            var output = _this._outputs[index];

            _this._outputs.splice(index, 1);
            _this.outputs = _createMIDIPortMap(_this._outputs);

            output._setState(MIDIPortDeviceState.disconnected, false);
            output._setConnection(MIDIPortConnectionState.pending, true);
        };

        _callback_removeSource = function(index) {
            if (!_this._inputs || _this._inputs.length == 0 || index > _this._inputs.length - 1) {
                console.log('_callback_removeSource failed: ' + index);
                return;
            }

            var input = _this._inputs[index];

            _this._inputs.splice(index, 1);
            _this.inputs = _createMIDIPortMap(_this._inputs);

            input._setState(MIDIPortDeviceState.disconnected, false);
            input._setConnection(MIDIPortConnectionState.pending, true);
        };

        if (typeof options !== "undefined") {
            if (options['sysex'] == true || options['sysex'] == "true") {
                this._sysexAccessRequested = true;
                options['sysex'] = true; // options['sysex'] must be a boolean value
            }
        }

        var param = { "options": options, "url" : document.location.href };

        //window.webkit.messageHandlers.onready.postMessage(param);
        console.log("call onready!");

        window.Android.onready(JSON.stringify(param));
    };

    function _onReady() {
        if (this._promise)
            this._promise.succeed(this);
    }

    function _onNotReady() {
        if (this._promise)
            this._promise.fail( { code: 1 } );
    }

    function _createMIDIPortMap(list) {
        var size = list.length,
            values = [],
            keys = [],
            entries = [],
            portsById = {},
            port, i;

        for(i = 0; i < size; i++) {
            port = list[i];

            entries.push([port.id, port]);
            values.push(port);
            keys.push(port.id);
            portsById[port.id] = port;
        }

        keys = new Iterator(keys);
        values = new Iterator(values);
        entries = new Iterator(entries);

        return {
            size: size,
            forEach: function(cb) {
                entries.reset();
                for (var entry = entries.next(); !entry.done; entry = entries.next()) {
                    cb(entry.value[1]);
                }
            },
            keys: function() {
                keys.reset();
                return keys;
            },
            values: function() {
                values.reset();
                return values;
            },
            entries: function() {
                entries.reset();
                return entries;
            },
            get: function(id) {
                return portsById[id];
            },
            has: function(id) {
                return portsById[id] !== undefined;
            }
        };
    }

    MIDIAccess.prototype = new MIDIEventDispatcher();


    MIDIPort = function MIDIPort() {
        this.id = 0;
        this.manufacturer = "";
        this.name = "";
        this.type = "";
        this.version = "";
        this.state = MIDIPortDeviceState.connected;
        this.connection = MIDIPortConnectionState.closed;

        this._promise = new Promise;

        this._midiaccess = null;
    };

    MIDIPort.prototype = new MIDIEventDispatcher();

    MIDIPort.prototype._setConnection = function(newState, isDispatch) {
        if (this.connection != newState) {
            this.connection = newState;

            if (isDispatch) {
                var evt = document.createEvent( "Event" );
                evt.initEvent( "statechange", false, false );
                evt.port = this;
                this.dispatchEvent(evt);

                if (this._midiaccess != null) {
                    this._midiaccess.dispatchEvent(evt);
                }
            }
        }
    };

    MIDIPort.prototype._setState = function(newState, isDispatch) {
        if (this.state != newState) {
            this.state = newState;

            if (isDispatch) {
                var evt = document.createEvent( "Event" );
                evt.initEvent( "statechange", false, false );
                evt.port = this;
                this.dispatchEvent(evt);

                if (this._midiaccess != null) {
                    this._midiaccess.dispatchEvent(evt);
                }
            }
        }
    };

    MIDIPort.prototype.open = function() {
        this._setConnection(MIDIPortConnectionState.open, true);

        setTimeout(function() {
            this._promise.succeed(this);
        }.bind(this), 0);

        return this._promise;
    };

    MIDIPort.prototype.close = function() {
        this._setConnection(MIDIPortConnectionState.closed, true);

        setTimeout(function() {
            this._promise.succeed(this);
        }.bind(this), 0);

        return this._promise;
    };


    MIDIInput = function MIDIInput( id, name, manufacturer, index, midiaccess ) {
        this._index = index;
        this.id = id;
        this.manufacturer = manufacturer;
        this.name = name;
        this.type = "input";
        this.version = "";
        this._onmidimessage = null;
        this._midiaccess = midiaccess;

        var _this = this;
        window.addEventListener('unload', function(e) { _this._midiaccess = null; }, false);
    };

    MIDIInput.prototype = new MIDIPort();

    Object.defineProperty(MIDIInput.prototype, "onmidimessage", {
        set: function (value) {
            this._onmidimessage = value;
            if (this.connection == MIDIPortConnectionState.closed) {
                this._setConnection(MIDIPortConnectionState.open, true);
            }
        },
        get : function () {
            return this._onmidimessage;
        }
    });

    MIDIOutput = function MIDIOutput( id, name, manufacturer, index, midiaccess ) {
        this._index = index;
        this.id = id;
        this.manufacturer = manufacturer;
        this.name = name;
        this.type = "output";
        this.version = "";
        this._midiaccess = midiaccess;

        var _this = this;
        window.addEventListener('unload', function(e) { _this._midiaccess = null; }, false);
    };

    MIDIOutput.prototype = new MIDIPort();

    MIDIOutput.prototype.send = function( data, timestamp ) {
        if (data.length === 0) {
            throw new TypeError();
            return false;
        }

        if (this._midiaccess.sysexEnabled == false) {
            var i = data.length;
            do {
                if (data[i] == 0xf0) {
                    throwException(DOMException.INVALID_ACCESS_ERR, "InvalidAccessError");
                }
            } while (i--);
        }

        if (this.state == MIDIPortDeviceState.disconnected) {
            throwException(DOMException.INVALID_STATE_ERR, "InvalidStateError");
        }

        if (this.connection == MIDIPortConnectionState.closed) {
            this._setConnection(MIDIPortConnectionState.open, true);
        }

        MIDIOutputData = function ( outputPortIndex, data, timestamp, now) {
            this.outputPortIndex = outputPortIndex;
            this.data = data.map(function(i) { return Number(i); }); // convert "data" to an array of Number
            this.timestamp = timestamp;
            this.now = now;
        };

        var outputData = new MIDIOutputData(this._index, data, timestamp, new Date().getTime());

        if (window.midiWebSocket) {
            var t1 = new Date().getTime()
            window.midiWebSocket.send(JSON.stringify(outputData));
            var t2 = new Date().getTime()
            console.log('websocket: delay[' + (t2-t1) + ']timestamp[' + timestamp+ "][" + JSON.stringify(outputData) + "]")
        } else {
            var t1 = new Date().getTime()
            window.Android.send(JSON.stringify(outputData));
            var t2 = new Date().getTime()
            console.log('delay[' + (t2-t1) + ']')
        }

        return true;
    };

    function throwException(code, name) {
        function DOMException(code, name) {
            this.code = code;
            this.name = name;
            this.message = name;
            this.toString = function() {
                return name;
            };
        }

        throw new DOMException(code, name);
    }

    MIDIOutput.prototype.clear = function() {
        MIDIOutputClearData = function ( outputPortIndex ) {
            this.outputPortIndex = outputPortIndex;
        };

        var outputClearData = new MIDIOutputClearData(this._index);

        //window.webkit.messageHandlers.clear.postMessage(outputClearData);

        console.log("clear!!!!" + JSON.stringify(outputClearData));
        window.Android.clear(JSON.stringify(outputClearData));
    };

    _requestMIDIAccess = function _requestMIDIAccess( options ) {
        console.log("_requestMIDIAccess!!!!");
        var access = new MIDIAccess( options );
        return access._promise;
    };

    if (!window.navigator.requestMIDIAccess) {
        console.log("window.navigator.requestMIDIAccess is empty!!!!");
    } else {
        console.log("window.navigator.requestMIDIAccess is not empty!!!!===");
    }

    window.navigator.requestMIDIAccess = _requestMIDIAccess;

    // init websocket
    var ws = new WebSocket("ws://localhost:7788/midi");
    ws.onopen = function () {
        console.log("WebSocket: open");
    };

    ws.onmessage = function(evt) {
        console.log("WebSocket: onmessage");
      console.log(evt.data);
    };

    ws.onclose = function(evt){
      console.log("WebSocket: WebSocketClosed!");
    };

    ws.onerror = function(evt) {
      console.log("WebSocket: WebSocketError!");
    };

    window.midiWebSocket = ws;

    window.androidMIDIReady = true;
    window.postMessage("androidMIDIReady", "*");
}(window));
